//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


namespace EffiProzDB
{

    /**
     * This class manages the reuse of CompiledStatement objects for prepared
     * statements for a Database instance.<p>
     *
     * A compiled statement is registered by a session to be managed. Once
     * registered, it is linked with one or more sessions.<p>
     *
     * The sql statement text distinguishes different compiled statements and acts
     * as lookup key when a session initially looks for an existing instance of
     * the compiled sql statement.<p>
     *
     * Once a session is linked with a statement, it uses the uniqe compiled
     * statement id for the sql statement to access the statement.<p>
     *
     * Changes to database structure via DDL statements, will result _in all
     * registered CompiledStatement objects to become invalidated. This is done by
     * setting to null all the managed CompiledStatement instances, while keeping
     * their id and sql string. When a session subsequently attempts to use an
     * invalidated (null) CompiledStatement via its id, it will reinstantiate the
     * CompiledStatement using its sql statement still held by this class.<p>
     *
     * This class keeps count of the number of different sessions that are linked
     * to each registered compiled statement, and the number of times each session
     * is linked.  It unregisters a compiled statement when no session remains
     * linked to it.<p>
     *
     * Modified by fredt@users from the original by boucherb@users to simplify,
     * support multiple identical prepared statements per session, and avoid
     * keeping references to CompiledStatement objects after DDL changes which
     * could result _in memory leaks. Modified further to support schemas.<p>
     *
     * @author boucherb@users
     * @author fredt@users
     *
     * @since 1.7.2
     * @version 1.8.0
     */
    public sealed class CompiledStatementManager
    {

        /**
         * The Database for which this object is managing
         * CompiledStatement objects.
         */
        private Database database;

        /** Map: Schema id (int) => {Map: SQL string => Compiled Statement id (int)} */
        private Dictionary<int, Dictionary<string, int>> schemaMap;

        /** Map: Compiled Statement id (int) => SQL string */
        private Dictionary<int, string> sqlLookup;

        /** Map: Compiled statment id (int) => CompiledStatement object. */
        private Dictionary<int, CompiledStatement> csidMap;

        /** Map: Session id (int) => {Map: compiled statement id (int) => use count _in session} */
        private Dictionary<int, Dictionary<int, int>> sessionUseMap;

        /** Map: Compiled statment id (int) => number of sessions that use the statement */
        private Dictionary<int, int> useMap;

        /**
         * Monotonically increasing counter used to assign unique ids to compiled
         * statements.
         */
        private int next_cs_id;

        /**
         * Constructs a new instance of <code>CompiledStatementManager</code>.
         *
         * @param database the Database instance for which this object is to
         *      manage compiled statement objects.
         */
        public CompiledStatementManager(Database database)
        {

            this.database = database;
            schemaMap = new Dictionary<int, Dictionary<string, int>>();
            sqlLookup = new Dictionary<int, string>();
            csidMap = new Dictionary<int, CompiledStatement>();
            sessionUseMap = new Dictionary<int, Dictionary<int, int>>();
            useMap = new Dictionary<int, int>();
            next_cs_id = 0;
        }

        /**
         * Clears all internal data structures, removing any references to compiled statements.
         */
        public void reset()
        {
            lock (this)
            {
                schemaMap.Clear();
                sqlLookup.Clear();
                csidMap.Clear();
                sessionUseMap.Clear();
                useMap.Clear();

                next_cs_id = 0;
            }
        }

        /**
         * Used after a DDL change that could impact the compiled statements.
         * Clears references to CompiledStatement objects while keeping the counts
         * and references to the sql strings.
         */
        public void resetStatements()
        {
            lock (this)
            {

                foreach (var cs in csidMap.Values)
                {

                    cs.clearVariables();
                }
            }

        }

        /**
         * Retrieves the next compiled statement identifier _in the sequence.
         *
         * @return the next compiled statement identifier _in the sequence.
         */
        private int nextID()
        {

            next_cs_id++;

            return next_cs_id;
        }

        /**
         * Retrieves the registered compiled statement identifier associated with
         * the specified SQL String, or a value less than zero, if no such
         * statement has been registered.
         *
         * @param schema the schema id
         * @param sql the SQL String
         * @return the compiled statement identifier associated with the
         *      specified SQL String
         */
        private int getStatementID(HsqlNameManager.HsqlName schema, string sql)
        {

            Dictionary<string, int> sqlMap = null;


            if (!schemaMap.TryGetValue(schema.GetHashCode(), out sqlMap))
            {
                return -1;
            }

            int id = -1;
            if (sqlMap.TryGetValue(sql, out id))
            {
                return id;
            }

            return -1;
        }

        /**
         * Returns an existing CompiledStatement object with the given
         * statement identifier. Returns null if the CompiledStatement object
         * has been invalidated and cannot be recompiled
         *
         * @param session the session
         * @param csid the identifier of the requested CompiledStatement object
         * @return the requested CompiledStatement object
         */
        public CompiledStatement getStatement(Session session, int csid)
        {
            lock (this)
            {

                CompiledStatement cs = null;

                if (!csidMap.TryGetValue(csid, out cs))
                {
                    return null;
                }

                if (!cs.isValid)
                {
                    string sql = (String)sqlLookup[(csid)];

                    // revalidate with the original schema
                    try
                    {
                        cs = compileSql(session, sql, cs.schemaHsqlName.name);
                        cs.id = csid;

                        csidMap.Add(csid, cs);
                    }
                    catch (Exception )
                    {
                        freeStatement(csid, session.getId(), true);

                        return null;
                    }
                }

                return cs;
            }
        }

        /**
         * Links a session with a registered compiled statement.
         *
         * If this session has not already been linked with the given
         * statement, then the statement use count is incremented.
         *
         * @param csid the compiled statement identifier
         * @param sid the session identifier
         */
        private void linkSession(int csid, int sid)
        {

            Dictionary<int, int> scsMap;


            if (!sessionUseMap.TryGetValue(sid, out scsMap))
            {
                scsMap = new Dictionary<int, int>();

                sessionUseMap.Add(sid, scsMap);
            }

            int count = 0;
            scsMap.TryGetValue(csid, out count);

            scsMap.Add(csid, count + 1);

            if (count == 0)
            {
                int tmp = 0;
                useMap.TryGetValue(csid, out tmp);
                useMap.Add(csid, tmp + 1);
            }
        }

        /**
         * Registers a compiled statement to be managed.
         *
         * The only caller should be a Session that is attempting to prepare
         * a statement for the first time or process a statement that has been
         * invalidated due to DDL changes.
         *
         * @param csid existing id or negative if the statement is not yet managed
         * @param cs The CompiledStatement to add
         * @return The compiled statement id assigned to the CompiledStatement
         *  object
         */
        private int registerStatement(int csid, CompiledStatement cs)
        {

            if (csid < 0)
            {
                csid = nextID();

                int schemaid = cs.schemaHsqlName.GetHashCode();
                Dictionary<string, int> sqlMap = null;



                if (!schemaMap.TryGetValue(schemaid, out sqlMap))
                {
                    sqlMap = new Dictionary<string, int>();

                    schemaMap.Add(schemaid, sqlMap);
                }

                sqlMap.Add(cs.sql, csid);
                sqlLookup.Add(csid, cs.sql);
            }

            cs.id = csid;

            csidMap.Add(csid, cs);

            return csid;
        }

        /**
         * Removes one (or all) of the links between a session and a compiled statement.
         *
         * If the statement is not linked with any other session, it is removed
         * from management.
         *
         * @param csid the compiled statment identifier
         * @param sid the session identifier
         * @param freeAll if true, remove all links to the session
         */
        public void freeStatement(int csid, int sid, bool freeAll)
        {

            if (csid == -1)
            {

                // statement was never added
                return;
            }

            Dictionary<int, int> scsMap = null;


            if (!sessionUseMap.TryGetValue(sid, out scsMap))
            {

                // statement already removed due to invalidation
                return;
            }

            int sessionUseCount = 0;


            if (!scsMap.TryGetValue(csid, out sessionUseCount))
            {

                // statement already removed due to invalidation
            }
            else if (sessionUseCount == 1 || freeAll)
            {
                scsMap.Remove(csid);

                int usecount = 0;

                if (!useMap.TryGetValue(csid, out usecount))
                {

                    // statement already removed due to invalidation
                }
                else if (usecount == 1)
                {

                    CompiledStatement cs = csidMap[csid];
                    csidMap.Remove(csid);

                    if (cs != null)
                    {
                        int schemaid = cs.schemaHsqlName.GetHashCode();
                        Dictionary<string, int> sqlMap = schemaMap[(schemaid)];

                        string sql = sqlLookup[csid];
                        sqlLookup.Remove(csid);
                        sqlMap.Remove(sql);
                    }

                    useMap.Remove(csid);
                }
                else
                {
                    useMap.Add(csid, usecount - 1);
                }
            }
            else
            {
                scsMap.Add(csid, sessionUseCount - 1);
            }
        }

        /**
         * Releases the link betwen the session and all compiled statement objects
         * it is linked to.
         *
         * If any such statement is not linked with any other session, it is
         * removed from management.
         *
         * @param sid the session identifier
         */
        public void removeSession(int sid) {
        lock(this){

            Dictionary<int,int> scsMap = null;     
     

            if (!sessionUseMap.TryGetValue(sid, out scsMap)) {
                return;
            }
            sessionUseMap.Remove(sid);


            foreach (var csid in scsMap.Keys) {
          
                int usecount;
                if (!useMap.TryGetValue(csid, out usecount))
                    usecount = -1;
                else
                    usecount--;

                if (usecount == 0) {
                    CompiledStatement cs = null;
               
                    if (csidMap.TryGetValue(csid, out cs) ) {
                        int schemaid = cs.schemaHsqlName.GetHashCode();
                        Dictionary<string, int> sqlMap = schemaMap[schemaid];
                           
                        string sql =sqlLookup[csid];
                        sqlLookup.Remove(csid);

                        sqlMap.Remove(sql);
                    }
                    csidMap.Remove(csid);
                    useMap.Remove(csid);
                } else {
                    useMap[csid] = usecount;
                }
            }
        }
    }

        /**
         * Retrieves a MULTI Result describing three aspects of the
         * CompiledStatement prepared from the SQL argument for execution
         * _in this session context. <p>
         *
         * <ol>
         * <li>A PREPARE_ACK mode Result describing id of the statement
         *     prepared by this request.  This is used by the JDBC implementation
         *     to later identify to the engine which prepared statement to execute.
         *
         * <li>A DATA mode result describing the statement's result set metadata.
         *     This is used to generate the JDBC ResultSetMetaData object returned
         *     by PreparedStatement[MetaData and CallableStatement[MetaData.
         *
         * <li>A DATA mode result describing the statement's parameter metdata.
         *     This is used to by the JDBC implementation to determine
         *     how to send parameters back to the engine when executing the
         *     statement.  It is also used to construct the JDBC ParameterMetaData
         *     object for PreparedStatements and CallableStatements.
         *
         * @param session the session
         * @param sql a string describing the desired statement object
         * @return a MULTI Result describing the compiled statement.
         */
        public CompiledStatement compile(Session session,
                                               string sql)
        {

            lock (this)
            {
                int csid = getStatementID(session.currentSchema, sql);
                CompiledStatement cs = null;
                csidMap.TryGetValue(csid, out cs);

                if (cs == null || !cs.isValid || !session.isAdmin())
                {
                    cs = compileSql(session, sql, session.currentSchema.name);
                    csid = registerStatement(csid, cs);
                }

                linkSession(csid, session.getId());

                return cs;
            }
        }

        private CompiledStatement compileSql(Session session, string sql,
                                             string schemaName)
        {

            Session sys = database.sessionManager.getSysSession(schemaName,
                session.getUser());

            return sys.sqlCompileStatement(sql);
        }
    }
}